﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;

namespace GoodStuff.Xml
{
    /// <summary>
    /// Merges two document trees together.
    /// </summary>
    /// <remarks>
    /// This merger class has been specifically developed to merge configuration files. The default node match algoritm 
    /// can separated repeating nodes by detecting a 'name=' or 'key=' attribute.
    /// </remarks>
    public class XmlMerger
    {
        private XmlNode _rootNode;

        public XmlMerger(XmlNode rootNode)
        {
            _rootNode = rootNode;
        }

        public void ApplyMerge(XmlNode transformNode, IDictionary<string, string> overrides)
        {
            ApplyRecursive(_rootNode, transformNode, overrides, 0);
        }

        protected XmlNode MatchNode(XmlNode sourceContainer, XmlNode targetNode)
        {
            var attribute = targetNode.Attributes["name"];
            if (attribute != null && !string.IsNullOrEmpty(attribute.Value))
            {
                return sourceContainer.SelectSingleNode(targetNode.Name + "[@name='" + attribute.Value + "']");
            }

            var attribute2 = targetNode.Attributes["key"];
            if (attribute2 != null && !string.IsNullOrEmpty(attribute2.Value))
            {
                return sourceContainer.SelectSingleNode(targetNode.Name + "[@key='" + attribute2.Value + "']");
            }

            return sourceContainer.SelectSingleNode(targetNode.Name);
        }

        private void ApplyRecursive(XmlNode sourceNode, XmlNode transformNode, IDictionary<string, string> overrides, int level)
        {
            foreach (XmlNode transformChild in transformNode.ChildNodes)
            {
                if (transformChild.NodeType == XmlNodeType.Element)
                {
                    XmlNode sourceChild = MatchNode(sourceNode, transformChild);
                    if (sourceChild == null)
                    {
                        //child not found, we can clone the entire section and be done with it.

                        //if (level == 0)
                        //{
                        //    //create a spacer level by inserting a blank line.
                        //    sourceNode.AppendChild(sourceNode.OwnerDocument.CreateWhitespace("\n\n\t"));
                        //}
                        sourceChild = sourceNode.OwnerDocument.CreateElement(transformChild.Name);
                        sourceNode.AppendChild(sourceChild);
                    }

                    //copy or apply all the attributes.
                    foreach (XmlAttribute transformAttribute in transformChild.Attributes)
                    {
                        XmlAttribute sourceAttribute = sourceChild.Attributes[transformAttribute.Name];
                        if (sourceAttribute == null)
                        {
                            sourceAttribute = sourceNode.OwnerDocument.CreateAttribute(transformAttribute.Name);
                            sourceChild.Attributes.Append(sourceAttribute);
                        }

                        string newValue = transformAttribute.Value;
                        if (overrides != null)
                        {
                            foreach (var param in overrides)
                            {
                                if (newValue.Contains("{{" + param.Key + "}}"))
                                {
                                    newValue = newValue.Replace("{{" + param.Key + "}}", param.Value);
                                }
                            }
                        }

                        sourceAttribute.Value = newValue;
                    }

                    ApplyRecursive(sourceChild, transformChild, overrides, level + 1);
                }
                else
                {
                    //if (transformChild.NodeType == XmlNodeType.Comment)
                    //{
                    //    //apply the comment too?
                    //    //yes, by all means.
                    //    XmlComment comment = sourceNode.OwnerDocument.CreateComment(transformChild.Value);

                    //    sourceNode.AppendChild(comment);
                    //}
                    //if (transformChild.NodeType == XmlNodeType.Whitespace)
                    //{
                    //    XmlWhitespace space = sourceNode.OwnerDocument.CreateWhitespace(transformChild.Value);

                    //    sourceNode.AppendChild(space);
                    //}
                    //else
                    if (transformChild.NodeType == XmlNodeType.Text)
                    {
                        //assert that the parent has ONLY ONE text node child.
                        if (sourceNode.FirstChild != sourceNode.LastChild)
                        {
                            throw new Exception("Cannot merge text with multiple child nodes or attributes");
                        }

                        sourceNode.InnerText = transformChild.InnerText;

                        //sourceNode.RemoveAll();
                        //XmlText space = sourceNode.OwnerDocument.CreateTextNode(transformChild.Value);

                        //sourceNode.AppendChild(space);
                    }
                }
            }
        }
    }
}
